fix(plugin-sharing): share-link enforcement 吃完整授权信封,修复 group 姿态建链恒 403 (#6206) - #6552
Merged
Merged
Conversation
…elope (#6206) The share-link routes rebuilt a four-field object out of the `resolveAuthzContext` result (`userId`/`tenantId`/`positions`/`permissions`) and handed it straight to `engine.find` as the [Finding-2] visibility check's context. `accessible_org_ids`, `org_user_ids`, `systemPermissions`, `posture` and `tabPermissions` were dropped on the way into enforcement. Under the `group` tenancy posture `accessible_org_ids` IS the Layer 0 wall (ADR-0105 D2) and an absent set denies, so the check failed closed and link creation answered 403 for records the caller reads fine elsewhere — reproduced here, not only read from the code. The envelope is now passed through whole (`{ ...authz, isSystem: false }`), per the maintainer's option-A ruling on #6206 and the contract half that landed with #6511. `posture` travels with the context and is never re-derived at the enforcement site (ADR-0095 D2). `ShareLinkExecutionContext` survives as the routes' own 401 vocabulary, consumed only by the new `isAuthenticated` gate. Tests: a seam-parity pin in plugin-sharing (the enforcement context must carry every key the real resolver produced — re-trimming fails by naming the dropped keys) and the behavioural `group`-posture repro in plugin-security, which owns `computeTenantLayer0Filter` and can therefore drive the real wall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 8, 2026 04:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6206
裁决 A 案(维护者 2026-08-07,comment 5219847367)的消费半边。契约半边已随 PR #6511(
d7e0b4212,#6430)落 main,本 PR 按落地后的契约形状消费。问题
share-link 路由在
resolveAuthzContext之后重新拼了一个四字段对象:这个对象不是路由自己用的身份 —— 它被原样交给
ShareLinkService.createLink,而后者用它去engine.find复读目标记录([Finding-2]「只能为自己看得见的记录建链接」)。于是accessible_org_ids、org_user_ids、systemPermissions、posture、tabPermissions在进 enforcement 的路上被丢掉。group租户姿态下accessible_org_ids就是 Layer 0 那堵墙(ADR-0105 D2),集合缺席即判否(fail closed)⇒ 可见性校验查不到任何行 ⇒ 路由抛 403。是可用性缺陷,不是泄露。改法
信封整个透传,不再逐字段挑选:
逐字段挑选正是这条缝出问题的方式 —— 换一份「这次写全了」的字段清单,只是把同一颗雷推迟到
ResolvedAuthzContext下次长出新维度的那天。posture因此随上下文流动,不在 enforcement 处重推(ADR-0095 D2)。isSystem: false与两个兄弟传输面(rest-server.ts/runtime/src/security/resolve-execution-context.ts)写法一致,也就是 ADR-0118 D2 的「absence is never system」显式化。窄类型
ShareLinkExecutionContext按裁决保留,但只服务路由自己的 401 判定:新增的isAuthenticated(ctx)是它在本仓库唯一的消费点 —— 在读处收窄,而不是在生产处收窄,这正是裁决的要点。share-link-service.ts三个裁决方法与canManageShares探针的形参随 #6511 收成完整ExecutionContext。before-red:分诊的 repro 义务已兑现(复现成立)
分诊 caveat 说得很明白:方向系代码读出、未跑复现,接手第一件事是先复现。已跑,方向属实。
新增
packages/plugins/plugin-security/src/share-link-tenant-wall.test.ts—— 放在 plugin-security 是因为判否的那一步在这里算(computeTenantLayer0Filter),而该包本来就 devDepend 了 plugin-sharing(controlled-by-parent-sharing.test.ts等先例),反向依赖则会成环。测试里真实的部分是:被 boot 的真插件(所以跑的是生产那条闭包)、真 share-link service、真墙;替身只有存储 —— 且RLS_DENY_FILTER是靠「匹配不到任何行」判否的,和生产同一机制,不是特判。把裁剪恢复回去、重建 dist 后:
修后 4/4 绿。两条在两个世界里都绿的用例是防自欺的:
single姿态 201 不变(墙在该姿态惰性),以及调用方不属于记录所在组织时依然 403 —— 变宽的是信封,不是权限。回归钉
packages/plugins/plugin-sharing/src/share-link-enforcement-context.test.ts不写死字段清单 —— 写死清单正是坏掉的那件工件。它拿 #5859 的 seam kit(bootRequestContext,走真resolveAuthzContext)为同一 principal 造一份参照信封,断言 enforcement 拿到的 context 不缺任何一个键。将来新增授权维度两边同时覆盖;有人再裁一刀,报错会把丢掉的键名列出来:外加卡片点名的四个键按名断言、401 逻辑不变(且 401 在任何 enforcement 之前判定)、以及看不见的记录仍旧 403。
验证
pnpm --filter @objectstack/plugin-sharing test→ 14 files / 363 tests 全绿pnpm --filter @objectstack/plugin-security test→ 36 files / 773 tests 全绿pnpm --filter @objectstack/runtime test(消费半径:dispatcher 面的 share-links domain)→ 110 files / 1606 tests 全绿typecheck:plugin-sharing / plugin-security / runtime 三包 Donecheck:engine-double-contract(两个新替身的update/delete均以assertEngineUpdateDispatch/assertEngineDeleteDispatch开场)、check:authz-resolver、check:route-envelope、check:adr-anchors、check:slot-lookup、check:error-code-casing、check:nul-bytes全绿范围外发现
runtime/src/domains/share-links.ts:78把已经完整的context.executionContext又裁成{ userId, tenantId }才喂给同样这三个裁决方法 —— 同一条 enforcement 路径的另一张脸,裁得更狠(连 positions/permissions 都没了)。查重过 finding: ExecutionContext 在 dispatcher / REST / share-link 三处独立组装 —— 收敛为单一共享装配函数前,先裁决匿名面分歧 #6216 / finding:SharingExecutionContext是同族第四个窄 enforcement 契约类型(sharing / approval / report 三个服务共用),#6206 裁决的「不留 per-site 子集」默认尚未覆盖它 #6523,不重复。已按 Prime Directive chore: version packages #10 立单不修(不在本单文件面)。⛔ 未触碰
sharing-plugin.ts:857 一带的 FORBIDDEN 抛点(#5493 领地);未动 spec。Generated by Claude Code